package scuts.profiler;
#if (macro || display)
import haxe.macro.Compiler;
import haxe.macro.Context;
import neko.FileSystem;
import neko.io.File;
import haxe.macro.Expr;
import scuts.Core;
import haxe.macro.Type;
import scuts.macro.Check;
import scuts.macro.ExtendedContext;
import scuts.macro.Make;
import scuts.macro.Print;
typedef SType = scuts.macro.Type;
#end
/**
 * ...
 * @author 
 */

class Boot
{
	#if (macro || display)
	private static var data:Array<String> = [];
	#end
	
	@:macro public static function register (id:String):Array<haxe.macro.Expr.Field> {
    
   
    
		trace("register: " + id);
		var fields = Context.getBuildFields();
		for (f in fields) {
			switch (f.kind) {
				case FFun(fn):
					registerFunction(id + "." + f.name, fn, f.name, f.pos);
				default:
			}
		}
		return fields;
	}
	#if (macro || display)
	
	public static function registerFunction (functionId:String, fn:Function, name:String, pos:Position) {
		
		//trace(functionId);
		if (functionId == null) throw "assert";
		if (fn.expr != null && !Check.isEmptyBlock(fn.expr)) {
			//make Block

			
			data.push(functionId);
			
			var exprStr1 = 'scuts.profiler.Profiler.enterFunction("' + functionId + '")';
			var expr1 = Context.parse(exprStr1, pos);
			var exprStr2 = 'scuts.profiler.Profiler.exitFunction()';
			var expr2 = Context.parse(exprStr2, pos);
			
      // isVoid is not safe for all void functions
			var isVoid = Check.isVoidFunction(fn);
			
			//var expr2 = ExprHelper.mkVarExpr('var __profilerTime = haxe.Timer.stamp()');
			
			// stop timer on return
			var fExpr = modifyFunctionExpr(functionId, fn.expr);
			
			var exprs = [expr1, fn.expr];
			if (isVoid && !Check.isBlockAndLastExprIsVoidReturn(fn.expr)) exprs.push(expr2);
			
			var newExpr = Make.mkBlock(exprs);
      
			fn.expr = newExpr;
      if (name == "release") {
        trace(Print.functionStr(fn));
      }
		}
	}
	
	public static function modifyFunctionExpr (functionId:String, expr:Expr):Void {
		if (expr == null) return;
		var mod = function (expr) {
			modifyFunctionExpr(functionId, expr);
		}
		switch (expr.expr) {
			case EBlock(exprs): 
				for (e in exprs) {
					modifyFunctionExpr(functionId, e);
				}
			
			case EReturn(e): 
				if (e != null) {
					var varExpr = Make.mkVarAssignExpr("__profiler_res", e);
					var newRetExpr = Context.parse("return __profiler_res", e.pos);
					var exprStr1 = 'scuts.profiler.Profiler.exitFunction()';
					var expr1 = Context.parse(exprStr1, expr.pos);
					var block = Make.mkBlock([varExpr, expr1, newRetExpr]);
					expr.expr = block.expr;
					
				} else {
					var exprStr1 = 'scuts.profiler.Profiler.exitFunction()';
					var expr1 = Context.parse(exprStr1, expr.pos);
					var retExpr = { expr:expr.expr, pos: expr.pos };
					var block = Make.mkBlock([expr1, retExpr]);
					expr.expr = block.expr;
				}
			case EIf(econd, eif, eelse):
				mod(econd);
				mod(eif);
				mod(eelse);
			case EWhile(econd, e, _):
				mod(econd);
				mod(e);
			case ETry(e, catches):
				mod(e);
				for (c in catches) {
					mod(c.expr);
				}
			case ESwitch(e, cases, edef):
				mod(e);
				mod(edef);
				for (c in cases) {
					mod(c.expr);
				}
			
			case EFunction(name, f):
				var locName = if (name != null) {
					"$" + name;
				} else {
					var infos = Context.getPosInfos(expr.pos);
					"$(" + infos.min + "-" + infos.max + ")";
				};
				registerFunction(functionId + "." + locName, f, name, expr.pos);
			
			case EFor(it, expr):
				mod(it);
				mod(expr);
			case EIn(e1, e2):
				mod(e1);
				mod(e2);
			case EVars(vars):
				for (v in vars) {
					mod(v.expr);
				}
			case EArray( e1 , e2 ):
				mod(e1);
				mod(e2);
				
			case EBinop( _ , e1 , e2 ):
				mod(e1);
				mod(e2);
			case EField( e , _ ):
				mod(e);
			case EType( e , _ ):
				mod(e);
				
			case EParenthesis( e ):
				mod(e);
			case EObjectDecl( fields ):
				for (f in fields) {
					mod(f.expr);
				}
			case EArrayDecl( values  ):
				for (v in values) mod(v);
				
			case ECall( e, params  ):
				// replace ecall with stoptime; call; starttime;
				mod(e);
				for (p in params) mod(p);
			case ENew( t , params ):
				for (p in params) mod(p);
			case EUnop( _, _, e ):
				mod(e);
			case EUntyped( e):
				mod(e);
				
				
			case EThrow( e ):
				mod(e);
				
			case ECast( e , _ ):
				mod(e);
			case ETernary( econd , eif , eelse ):
				mod(econd);
				mod(eif);
				mod(eelse);
			default:
		}
	}
	
	public static function applyToClassPaths(classpaths:Array<String>) 
	{
    
    
		Context.onGenerate(function (_) {
      // register all available functions in Profiler metaData
      // results in something like @fn(["Test.main", "my.pack.MyClass.myFunction"])
			var profilerType = Context.getType("scuts.profiler.Profiler");
			switch (profilerType) {
				case TInst(ct, _):
					var dataStr = '["' + data.join('", "') + '"]';
					
					var expr = Context.parse(dataStr, Context.currentPos());
					ct.get().meta.add("fn", [expr], Context.currentPos());
				default:
			}

		});
		for (c in classpaths) {
			var filter = function (t) {
				//trace(t.name);
				return !StringTools.startsWith(t.name, "scuts.profiler") && !StringTools.startsWith(t.name, "haxe.Timer");
			}
			var types = ExtendedContext.getTypesFromClasspath(c, filter);
			for (t in types) {
				//trace("metaName: " + t.name);
				Compiler.addMetadata("@:build(scuts.profiler.Boot.register('"+ t.name + "'))", t.name);
			}
		}
	}
	#end
	
	
	
	
}